/* *
 * --ライセンスについて--
 *
 * 「本ファイルの内容は Mozilla Public License Version 1.1 (「本ライセンス」)
 * の適用を受けます。
 * 本ライセンスに従わない限り本ファイルを使用することはできません。
 * 本ライセンスのコピーは http://www.mozilla.org/MPL/ から入手できます。
 *
 * 本ライセンスに基づき配布されるソフトウェアは、「現状のまま」で配布されるものであり、
 * 明示的か黙示的かを問わず、いかなる種類の保証も行われません。
 * 本ライセンス上の権利および制限を定める具体的な文言は、本ライセンスを参照してください。
 *
 * オリジナルコードおよび初期開発者は、N_H (h.10x64@gmail.com) です。
 *
 * N_H によって作成された部分の著作権表示は次のとおりです。
 *
 * Copyright (C) 2011 - 2012
 *
 * このファイルの内容は、上記に代えて、
 * GNU General License version2 以降 (以下 GPL とする)、
 * GNU Lesser General Public License Version 2.1 以降 (以下 LGPL とする)、
 * の条件に従って使用することも可能です。
 * この場合、このファイルの使用には上記の条項ではなく GPL または LGPL の条項が適用されます。
 * このファイルの他者による使用を GPL または LGPL の条件によってのみ許可し、
 * MPL による使用を許可したくない対象者は、上記の条項を削除することでその意思を示し、
 * 上記条項を GPL または LGPL で義務付けられている告知およびその他の条項に置き換えてください。
 * 対象者が上記の条項を削除しない場合、
 * 受領者は MPL または GPL または LGPL ライセンスのいずれによってもこのファイルを
 * 使用することができます。」
 *
 * -- License --
 *
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License。You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND、either express or implied。See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Initial Developer of the Original Code is
 *   N_H (h.10x64@gmail.com).
 *
 * Portions created by the Initial Developer are Copyright (C) 2011 - 2012
 * the Initial Developer。All Rights Reserved.
 *
 * Alternatively、the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL")、or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above。If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL、and not to allow others to
 * use your version of this file under the terms of the MPL、indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL。If you do not delete
 * the provisions above、a recipient may use your version of this file under
 * the terms of any one of the MPL、the GPL or the LGPL.
 *
 * */
package com.magiciansforest.audio.vst.binaural;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 *
 * @author N_H <h.10x64@gmail.com>
 */
public class Knob extends JComponent implements MouseListener, MouseMotionListener {

    private static final int BOX_WIDTH = 2;
    private boolean drag;
    private int tick;
    private double radian;
    private double max, min;
    private double rot;
    private BufferedImage bg, pointerImg, fg;
    private List<ChangeListener> changeListeners;
    private boolean isSelected = false;

    public Knob(double initialDeg, double minDeg, double maxDeg, double rot, int tick,
            BufferedImage background, BufferedImage pointer, BufferedImage foreground) {
        super();
        addMouseListener(this);
        addMouseMotionListener(this);

        this.tick = tick;
        this.bg = background;
        this.pointerImg = pointer;
        this.fg = foreground;
        changeListeners = new ArrayList<ChangeListener>();
        setMaxDegree(maxDeg);
        setMinDegree(minDeg);
        setRotDegree(rot);

        setDegree(initialDeg);
    }

    @Override
    public void setSize(int width, int height) {
        super.setSize(width, height);
        super.setPreferredSize(new Dimension(width, height));
    }

    @Override
    public void setSize(Dimension d) {
        super.setSize(d);
        super.setPreferredSize(d);
    }

    public void mouseClicked(MouseEvent e) {
        movePointer(e.getPoint());
    }

    public void mouseEntered(MouseEvent e) {
        isSelected = true;
        repaint();
    }

    public void mouseExited(MouseEvent e) {
        isSelected = false;
        repaint();
    }

    public void mousePressed(MouseEvent e) {
        drag = true;
    }

    public void mouseReleased(MouseEvent e) {
        drag = false;
    }

    public void mouseDragged(MouseEvent e) {
        if (drag) {
            movePointer(e.getPoint());
        }
    }

    public void mouseMoved(MouseEvent e) {
        if (drag) {
            movePointer(e.getPoint());
        }
    }

    public boolean addChangeListener(ChangeListener listener) {
        return changeListeners.add(listener);
    }

    public boolean removeChangeListener(ChangeListener listener) {
        return changeListeners.remove(listener);
    }

    public void setBackgroundImage(BufferedImage img) {
        this.bg = img;
    }

    public void setPointerImage(BufferedImage img) {
        this.pointerImg = img;
    }

    public void setForegroundImage(BufferedImage img) {
        this.fg = img;
    }

    public void setRotDegree(double rot) {
        setRotRadian(Math.toRadians(rot));
    }

    public void setRotRadian(double rot) {
        double r = rot % (2 * Math.PI);
        if (r != this.rot) {
            this.rot = r;
            repaint();
            fireChangeEvent();
        }
    }

    public void setMaxDegree(double max) {
        setMaxRadian(Math.toRadians(max));
    }

    public void setMaxRadian(double max) {
        double r = max % (2 * Math.PI);
        if (r != this.max) {
            this.max = r;
            repaint();
            fireChangeEvent();
        }
    }

    public void setMinDegree(double min) {
        setMinRadian(Math.toRadians(min));
    }

    public void setMinRadian(double min) {
        double r = min % (2 * Math.PI);
        if (r != this.min) {
            this.min = r;
            repaint();
            fireChangeEvent();
        }
    }

    public double getDegree() {
        return Math.toDegrees(getRadian());
    }

    public void setDegree(double degree) {
        setRadian(Math.toRadians(degree % 360));
    }

    public double getPercent() {
        if (max == min) {
            return 100.0 * getRadian() / (2 * Math.PI);
        } else {
            return 100.0 * (getRadian() - min) / (max - min);
        }
    }

    public void setPercent(double val) {
        if (max == min) {
            setRadian((val % 100.0) / 100.0 * (2 * Math.PI));
        } else {
            setRadian((max - min) * (val % 100.0) / 100.0 + min);
        }
    }

    public void setRadian(double radian) {
        double r = radian % (2 * Math.PI);
        if (r != this.radian) {
            this.radian = radian % (2 * Math.PI);
            if (this.radian < 0) {
                this.radian += 2 * Math.PI;
            }
            if (max != min) {
                this.radian = Math.min(max, Math.max(min, this.radian));
            }
            repaint();
            fireChangeEvent();
        }
    }

    public double getRadian() {
        return radian;
    }

    @Override
    public void paint(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;

        Insets insets = getInsets();
        int w = getWidth() - (insets.left + insets.right);
        int h = getHeight() - (insets.top + insets.bottom);
        int cx = w / 2;
        int cy = h / 2;
        int size = Math.round(Math.min(w, h) / 1.5f);

        if (bg != null) {
            int bgSize = Math.min(bg.getWidth(), bg.getHeight());
            AffineTransform trans = AffineTransform.getTranslateInstance(cx, cy);
            if (bgSize != size) {
                double ratio = (double) size / bgSize;
                trans.concatenate(AffineTransform.getScaleInstance(ratio, ratio));
            }
            trans.concatenate(AffineTransform.getTranslateInstance(-bg.getWidth() / 2,
                    -bg.getHeight() / 2));
            g2d.drawImage(bg, trans, this);
        }

        if (isSelected) {
            g2d.setColor(new Color(0.25f, 0, 0));
            g2d.setStroke(new BasicStroke(BOX_WIDTH));
            g2d.drawRect(BOX_WIDTH, BOX_WIDTH, getWidth() - 2 * BOX_WIDTH, getHeight() - 2 * BOX_WIDTH);
            g2d.setColor(new Color(0.2f, 0, 0));
            g2d.drawRect(2 * BOX_WIDTH, 2 * BOX_WIDTH, getWidth() - 4 * BOX_WIDTH, getHeight() - 4 * BOX_WIDTH);
            g2d.setStroke(new BasicStroke(1));
        }

        if (tick > 0) {
            double end = size * 8 / 12;
            double start = size * 7 / 12;
            for (int i = 0; i <= tick; i++) {
                double marker = (double) i / tick;
                if (min == max) {
                    marker *= 2 * Math.PI;
                } else {
                    marker *= (max - min);
                }
                marker += rot + Math.PI / 2;
                if (i == 0 || i == tick) {
                    g2d.setColor(Color.red.brighter());
                } else if (Math.round(Math.toDegrees(marker)) % 90 == 0) {
                    g2d.setColor(Color.red.darker());
                } else {
                    g2d.setColor(Color.gray);
                }
                g2d.drawLine(cx + ((int) Math.round(Math.cos(marker) * start)),
                        cy + ((int) Math.round(Math.sin(marker) * start)),
                        cx + ((int) Math.round(Math.cos(marker) * end)),
                        cy + ((int) Math.round(Math.sin(marker) * end)));
            }
        }

        if (pointerImg != null) {
            double pointerPos = radian + Math.PI;
            int pointerSize = Math.min(pointerImg.getWidth(), pointerImg.getHeight());
            AffineTransform trans = AffineTransform.getTranslateInstance(cx, cy);
            trans.concatenate(AffineTransform.getRotateInstance(pointerPos));
            if (pointerSize != size) {
                double ratio = (double) size / pointerSize;
                trans.concatenate(AffineTransform.getScaleInstance(ratio, ratio));
            }
            trans.concatenate(AffineTransform.getTranslateInstance(
                    -pointerImg.getWidth() / 2, -pointerImg.getHeight() / 2));
            g2d.drawImage(pointerImg, trans, this);
        } else {
            g2d.setColor(Color.gray);
            double l = size * 1 / 2;
            g2d.drawLine(cx, cy, cx + ((int) Math.round(Math.cos(radian + Math.PI / 2 + rot) * l)),
                    cy + ((int) Math.round(Math.sin(radian + Math.PI / 2 + rot) * l)));
        }

        if (fg != null) {
            int bgSize = Math.min(fg.getWidth(), fg.getHeight());
            AffineTransform trans = AffineTransform.getTranslateInstance(cx, cy);
            if (bgSize != size) {
                double ratio = (bgSize < size) ? (double) size / bgSize : (double) bgSize / size;
                trans.concatenate(AffineTransform.getScaleInstance(ratio, ratio));
            }
            trans.concatenate(AffineTransform.getTranslateInstance(-fg.getWidth() / 2,
                    -fg.getHeight() / 2));
            g2d.drawImage(fg, trans, this);
        }
    }

    private void movePointer(Point p) {
        double x = p.getX() - getWidth() / 2;
        double y = p.getY() - getHeight() / 2;

        double theta = Math.atan2(x, -y) + Math.PI - rot;
        if (max != min) {
            theta = Math.min(max, Math.max(min, theta));
        }
        setRadian(theta);
        fireChangeEvent();
    }

    private void fireChangeEvent() {
        ChangeEvent e = new ChangeEvent(this);
        Iterator<ChangeListener> itr = changeListeners.iterator();
        while (itr.hasNext()) {
            ChangeListener listener = itr.next();
            listener.stateChanged(e);
        }
    }
}
